/*
 *  ApplePS2ElantechTouchpad.cpp
 *  ApplePS2Trackpad
 *
 *  Created by Evan Lojewski on 7/24/09.
 *  Copyright 2009. All rights reserved.
 *
 */


#include <IOKit/assert.h>
#include <IOKit/IOLib.h>
#include <IOKit/hidsystem/IOHIDParameter.h>
#include "ApplePS2ElantechTouchpad.h"

enum {
    //
    //
    kTapEnabled  = 0x01
};

// =============================================================================
// ApplePS2ElantechTouchpad Class Implementation
//

#define super IOHIPointing
OSDefineMetaClassAndStructors(ApplePS2ElantechTouchpad, IOHIPointing);

UInt32 ApplePS2ElantechTouchpad::deviceType()
{ return NX_EVS_DEVICE_TYPE_MOUSE; };

UInt32 ApplePS2ElantechTouchpad::interfaceID()
{ return NX_EVS_DEVICE_INTERFACE_BUS_ACE; };

IOItemCount ApplePS2ElantechTouchpad::buttonCount() { return 2; };
IOFixed     ApplePS2ElantechTouchpad::resolution()  { return _resolution; };

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

bool ApplePS2ElantechTouchpad::init( OSDictionary * properties )
{
    //
    // Initialize this object's minimal state. This is invoked right after this
    // object is instantiated.
    //
    
    if (!super::init(properties))  return false;
	
    _device                    = 0;
    _interruptHandlerInstalled = false;
    _packetByteCount           = 0;
    _resolution                = (100) << 16; // (100 dpi, 4 counts/mm)
    _touchPadModeByte          = kTapEnabled;
	
    return true;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

ApplePS2ElantechTouchpad *
ApplePS2ElantechTouchpad::probe( IOService * provider, SInt32 * score )
{

    //
    // The driver has been instructed to verify the presence of the actual
    // hardware we represent. We are guaranteed by the controller that the
    // mouse clock is enabled and the mouse itself is disabled (thus it
    // won't send any asynchronous mouse data that may mess up the
    // responses expected by the commands we send it).
    //
	
    ApplePS2MouseDevice * device  = (ApplePS2MouseDevice *) provider;
    PS2Request *          request = device->allocateRequest();
    bool                  success = false;
    
    if (!super::probe(provider, score) || !request) return 0;
	
    //
    // Send an "Identify TouchPad" command and see if the device is
    // a ALPS GlidePoint based on its response.  End the command
    // chain with a "Set Defaults" command to clear all state.
    //
	
    // ALPS GlidePoint detection
    request->commands[0].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[0].inOrOut  = kDP_SetDefaultsAndDisable;
	
    // 3X set mouse scaling 1 to 1
    request->commands[1].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[1].inOrOut  = kDP_SetMouseScaling1To1;
    request->commands[2].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[2].inOrOut  = kDP_SetMouseScaling1To1;
    request->commands[3].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[3].inOrOut  = kDP_SetMouseScaling1To1;
	
    // get mouse info
    request->commands[4].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[4].inOrOut  = kDP_GetMouseInformation;
    request->commands[5].command  = kPS2C_ReadDataPort;
    request->commands[5].inOrOut  = 0;
    request->commands[6].command = kPS2C_ReadDataPort;
    request->commands[6].inOrOut = 0;
    request->commands[7].command = kPS2C_ReadDataPort;
    request->commands[7].inOrOut = 0;
    request->commands[8].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[8].inOrOut  = kDP_SetDefaultsAndDisable;
    request->commandsCount = 9;

	
	
    device->submitRequestAndBlock(request);
	
	if (request->commands[5].inOrOut != 0x3c || 
		request->commands[6].inOrOut != 0x03 || 
		request->commands[7].inOrOut != 0xc8) {
		success = false;
	} else
	{
		// TODO: Validate touhcpad version, also, the version string is not created properly
		
		_touchPadVersion = getTouchPadData(kET_firmwareVersion) >> 8;

	}
	
	
    device->freeRequest(request);
	
    return (success) ? this : 0;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

bool ApplePS2ElantechTouchpad::start( IOService * provider )
{ 
    UInt64 gesturesEnabled;
	
    //
    // The driver has been instructed to start. This is called after a
    // successful probe and match.
    //
	
    if (!super::start(provider)) return false;
	
    //
    // Maintain a pointer to and retain the provider object.
    //
	
    _device = (ApplePS2MouseDevice *) provider;
    _device->retain();
	
    //
    // Announce hardware properties.
    //
	
    IOLog("ApplePS2Trackpad: Elantech Touchpad v%d.%d\n",
          (UInt8)(_touchPadVersion >> 8), (UInt8)(_touchPadVersion));
	
    //
    // Write the TouchPad mode byte value.
    //
	
    setTapEnable(_touchPadModeByte);
	
    //
    // Advertise the current state of the tapping feature.
    //
	
    gesturesEnabled = (_touchPadModeByte == kTapEnabled)
	? 1 : 0;
    setProperty("Clicking", gesturesEnabled, sizeof(gesturesEnabled)*8);
	
    //
    // Must add this property to let our superclass know that it should handle
    // trackpad acceleration settings from user space.  Without this, tracking
    // speed adjustments from the mouse prefs panel have no effect.
    //
	
    setProperty(kIOHIDPointerAccelerationTypeKey, kIOHIDTrackpadAccelerationType);
	
    //
    // Install our driver's interrupt handler, for asynchronous data delivery.
    //
	
    _device->installInterruptAction(this,
									OSMemberFunctionCast(PS2InterruptAction, this, &ApplePS2ElantechTouchpad::interruptOccurred));
    _interruptHandlerInstalled = true;
	
    //
    // Enable the mouse clock (should already be so) and the mouse IRQ line.
    //
	
    setCommandByte( kCB_EnableMouseIRQ, kCB_DisableMouseClock );
	
    //
    // Finally, we enable the trackpad itself, so that it may start reporting
    // asynchronous events.
    //
	
    setTouchPadEnable(true);
	
    //
	// Install our power control handler.
	//
	
	_device->installPowerControlAction(this,
									   OSMemberFunctionCast(PS2PowerControlAction, this, &ApplePS2ElantechTouchpad::setDevicePowerState));
	_powerControlHandlerInstalled = true;
	
    return true;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

void ApplePS2ElantechTouchpad::stop( IOService * provider )
{
    //
    // The driver has been instructed to stop.  Note that we must break all
    // connections to other service objects now (ie. no registered actions,
    // no pointers and retains to objects, etc), if any.
    //
	
    assert(_device == provider);
	
    //
    // Disable the mouse itself, so that it may stop reporting mouse events.
    //
	
    setTouchPadEnable(false);
	
    //
    // Disable the mouse clock and the mouse IRQ line.
    //
	
    setCommandByte( kCB_DisableMouseClock, kCB_EnableMouseIRQ );
	
    //
    // Uninstall the interrupt handler.
    //
	
    if ( _interruptHandlerInstalled )  _device->uninstallInterruptAction();
    _interruptHandlerInstalled = false;
	
    //
    // Uninstall the power control handler.
    //
	
    if ( _powerControlHandlerInstalled ) _device->uninstallPowerControlAction();
    _powerControlHandlerInstalled = false;
	
	super::stop(provider);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

void ApplePS2ElantechTouchpad::free()
{
    //
    // Release the pointer to the provider object.
    //
	
    if (_device)
    {
        _device->release();
        _device = 0;
    }
	
    super::free();
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

void ApplePS2ElantechTouchpad::interruptOccurred( UInt8 data )
{
    //
    // This will be invoked automatically from our device when asynchronous
    // events need to be delivered. Process the trackpad data. Do NOT issue
    // any BLOCKING commands to our device in this context.
    //
    // Ignore all bytes until we see the start of a packet, otherwise the
    // packets may get out of sequence and things will get very confusing.
    //
    if (_packetByteCount == 0 && ((data == kSC_Acknowledge) || !(data & 0x08)))
    {
        return;
    }
	
    //
    // Add this byte to the packet buffer. If the packet is complete, that is,
    // we have the three bytes, dispatch this packet for processing.
    //
	
    _packetBuffer[_packetByteCount++] = data;
    
    if (_packetByteCount == 3)
    {
        dispatchRelativePointerEventWithPacket(_packetBuffer, 3);
        _packetByteCount = 0;
    }
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

void ApplePS2ElantechTouchpad::
dispatchRelativePointerEventWithPacket( UInt8 * packet,
									   UInt32  packetSize )
{
    //
    // Process the three byte relative format packet that was retreived from the
    // trackpad. The format of the bytes is as follows:
    //
    //  7  6  5  4  3  2  1  0
    // -----------------------
    // YO XO YS XS  1  M  R  L
    // X7 X6 X5 X4 X3 X3 X1 X0  (X delta)
    // Y7 Y6 Y5 Y4 Y3 Y2 Y1 Y0  (Y delta)
    //
	
    UInt32       buttons = 0;
    SInt32       dx, dy;
    AbsoluteTime now;
	
    if ( (packet[0] & 0x1) ) buttons |= 0x1;  // left button   (bit 0 in packet)
    if ( (packet[0] & 0x2) ) buttons |= 0x2;  // right button  (bit 1 in packet)
    if ( (packet[0] & 0x4) ) buttons |= 0x4;  // middle button (bit 2 in packet)
    
    dx = ((packet[0] & 0x10) ? 0xffffff00 : 0 ) | packet[1];
    dy = -(((packet[0] & 0x20) ? 0xffffff00 : 0 ) | packet[2]);
	
    clock_get_uptime((uint64_t *)&now);
    
    dispatchRelativePointerEvent(dx, dy, buttons, now);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

void ApplePS2ElantechTouchpad::setTapEnable( bool enable )
{
    //
    // Instructs the trackpad to honor or ignore tapping
    //
	bool success;
    PS2Request * request = _device->allocateRequest();
    if ( !request ) return;
	
    request->commands[0].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[0].inOrOut  = kDP_SetMouseScaling2To1;
    request->commands[1].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[1].inOrOut  = kDP_SetMouseScaling2To1;
    request->commands[2].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[2].inOrOut  = kDP_SetMouseScaling2To1;
    request->commands[3].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[3].inOrOut  = kDP_SetDefaultsAndDisable;
    request->commands[4].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[4].inOrOut  = kDP_GetMouseInformation;
    request->commands[5].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[5].inOrOut  = kDP_SetDefaultsAndDisable;
    request->commands[6].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[6].inOrOut  = kDP_SetDefaultsAndDisable;
	
    if (enable)
	{
		request->commands[7].command  = kPS2C_SendMouseCommandAndCompareAck;
    	request->commands[7].inOrOut  = kDP_SetMouseSampleRate;
		request->commands[8].command  = kPS2C_SendMouseCommandAndCompareAck;
    	request->commands[8].inOrOut  = 0x0A;	// 1010 somehow enables tapping
	}
	else
	{
		request->commands[7].command  = kPS2C_SendMouseCommandAndCompareAck;
    	request->commands[7].inOrOut  = kDP_SetMouseResolution;
		request->commands[8].command  = kPS2C_SendMouseCommandAndCompareAck;
    	request->commands[8].inOrOut  = 0x00;
	}
	
    request->commands[9].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[9].inOrOut  = kDP_SetMouseScaling1To1;
    request->commands[10].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[10].inOrOut  = kDP_SetMouseScaling1To1;
    request->commands[11].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[11].inOrOut  = kDP_SetMouseScaling1To1;
    request->commands[12].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[12].inOrOut  = kDP_SetDefaultsAndDisable;
    request->commands[13].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[13].inOrOut  = kDP_Enable;
	
    request->commandsCount = 14;
    // _device->submitRequest(request); // asynchronous, auto-free'd
	_device->submitRequestAndBlock(request);
	
    success = (request->commandsCount == 14);
	if (success)
	{
		setSampleRateAndResolution();
	}
	
    _device->freeRequest(request);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void ApplePS2ElantechTouchpad::setTouchPadEnable( bool enable )
{
    //
    // Instructs the trackpad to start or stop the reporting of data packets.
    // It is safe to issue this request from the interrupt/completion context.
    //
	
    PS2Request * request = _device->allocateRequest();
    if ( !request ) return;
	
    // (mouse enable/disable command)
    request->commands[0].command = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[0].inOrOut = kDP_SetDefaultsAndDisable;
    request->commands[1].command = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[1].inOrOut = kDP_SetDefaultsAndDisable;
    request->commands[2].command = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[2].inOrOut = kDP_SetDefaultsAndDisable;
    request->commands[3].command = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[3].inOrOut = kDP_SetDefaultsAndDisable;
	
	// (mouse or pad enable/disable command)
    request->commands[4].command = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[4].inOrOut = (enable)?kDP_Enable:kDP_SetDefaultsAndDisable;
    request->commandsCount = 5;
    _device->submitRequest(request); // asynchronous, auto-free'd
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

void ApplePS2ElantechTouchpad::setSampleRateAndResolution( void )
{
	// Following the Synaptics specification, but assuming everywhere it
	// says "Syanptics doesn't do this" implies "ALPS should do this"
	// We set the sample rate to 100, and resolution to 4 counts per mm
	// This should match the Synaptics behavior
    PS2Request * request = _device->allocateRequest();
    if ( !request ) return;
	
	// From 3.4 of the Synaptics TouchPad Interfacing Guide
    request->commands[0].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[0].inOrOut  = kDP_SetDefaultsAndDisable;
	request->commands[1].command = kPS2C_SendMouseCommandAndCompareAck;
	request->commands[1].inOrOut = kDP_SetMouseSampleRate;
	request->commands[2].command = kPS2C_SendMouseCommandAndCompareAck;
	request->commands[2].inOrOut = 100;
	request->commands[3].command = kPS2C_SendMouseCommandAndCompareAck;
	request->commands[3].inOrOut = kDP_SetMouseResolution;
	request->commands[4].command = kPS2C_SendMouseCommandAndCompareAck;
	request->commands[4].inOrOut = 2;   // 0x02 = 4 counts per mm
	request->commands[5].command = kPS2C_SendMouseCommandAndCompareAck;
	request->commands[5].inOrOut = kDP_Enable;
	request->commandsCount = 6;
	_device->submitRequestAndBlock(request);
	
    _device->freeRequest(request);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

void ApplePS2ElantechTouchpad::setCommandByte( UInt8 setBits, UInt8 clearBits )
{
    //
    // Sets the bits setBits and clears the bits clearBits "atomically" in the
    // controller's Command Byte.   Since the controller does not provide such
    // a read-modify-write primitive, we resort to a test-and-set try loop.
    //
    // Do NOT issue this request from the interrupt/completion context.
    //
	
    UInt8        commandByte;
    UInt8        commandByteNew;
    PS2Request * request = _device->allocateRequest();
	
    if ( !request ) return;
	
    do
    {
        // (read command byte)
        request->commands[0].command = kPS2C_WriteCommandPort;
        request->commands[0].inOrOut = kCP_GetCommandByte;
        request->commands[1].command = kPS2C_ReadDataPort;
        request->commands[1].inOrOut = 0;
        request->commandsCount = 2;
        _device->submitRequestAndBlock(request);
		
        //
        // Modify the command byte as requested by caller.
        //
		
        commandByte    = request->commands[1].inOrOut;
        commandByteNew = (commandByte | setBits) & (~clearBits);
		
        // ("test-and-set" command byte)
        request->commands[0].command = kPS2C_WriteCommandPort;
        request->commands[0].inOrOut = kCP_GetCommandByte;
        request->commands[1].command = kPS2C_ReadDataPortAndCompare;
        request->commands[1].inOrOut = commandByte;
        request->commands[2].command = kPS2C_WriteCommandPort;
        request->commands[2].inOrOut = kCP_SetCommandByte;
        request->commands[3].command = kPS2C_WriteDataPort;
        request->commands[3].inOrOut = commandByteNew;
        request->commandsCount = 4;
        _device->submitRequestAndBlock(request);
		
        //
        // Repeat this loop if last command failed, that is, if the
        // old command byte was modified since we first read it.
        //
		
    } while (request->commandsCount != 4);  
	
    _device->freeRequest(request);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

IOReturn ApplePS2ElantechTouchpad::setParamProperties( OSDictionary * dict )
{
    OSNumber * clicking = OSDynamicCast( OSNumber, dict->getObject("Clicking") );
	
    if ( clicking )
    {    
        UInt8  newModeByteValue = clicking->unsigned32BitValue() & 0x1 ?
		kTapEnabled :
		0;
		
        if (_touchPadModeByte != newModeByteValue)
        {
            _touchPadModeByte = newModeByteValue;
			
            //
            // Write the TouchPad mode byte value.
            //
			
            setTapEnable(_touchPadModeByte);
			
            //
            // Advertise the current state of the tapping feature.
            //
			
            setProperty("Clicking", clicking);
        }
    }
	
    return super::setParamProperties(dict);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

void ApplePS2ElantechTouchpad::setDevicePowerState( UInt32 whatToDo )
{
    switch ( whatToDo )
    {
        case kPS2C_DisableDevice:
            
            //
            // Disable touchpad.
            //
			
            setTouchPadEnable( false );
            break;
			
        case kPS2C_EnableDevice:
			
            setTapEnable( _touchPadModeByte );
			
			
            //
            // Enable the mouse clock (should already be so) and the
            // mouse IRQ line.
            //
			
            setCommandByte( kCB_EnableMouseIRQ, kCB_DisableMouseClock );
			
            //
            // Finally, we enable the trackpad itself, so that it may
            // start reporting asynchronous events.
            //
			
            setTouchPadEnable( true );
            break;
	}
}


UInt32 ApplePS2ElantechTouchpad::getTouchPadData( UInt8 dataSelector )
{
    PS2Request * request     = _device->allocateRequest();
    UInt32       returnValue = (UInt32)(-1);
	
    if ( !request ) return returnValue;
	
    // Disable stream mode before the command sequence.
    request->commands[0].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[0].inOrOut  = kDP_SetDefaultsAndDisable;
	
    // 4 set resolution commands, each encode 2 data bits.
    request->commands[1].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[1].inOrOut  = kDP_SetMouseResolution;
    request->commands[2].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[2].inOrOut  = (dataSelector >> 6) & 0x3;
	
    request->commands[3].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[3].inOrOut  = kDP_SetMouseResolution;
    request->commands[4].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[4].inOrOut  = (dataSelector >> 4) & 0x3;
	
    request->commands[5].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[5].inOrOut  = kDP_SetMouseResolution;
    request->commands[6].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[6].inOrOut  = (dataSelector >> 2) & 0x3;
	
    request->commands[7].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[7].inOrOut  = kDP_SetMouseResolution;
    request->commands[8].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[8].inOrOut  = (dataSelector >> 0) & 0x3;
	
    // Read response bytes.
    request->commands[9].command  = kPS2C_SendMouseCommandAndCompareAck;
    request->commands[9].inOrOut  = kDP_GetMouseInformation;
    request->commands[10].command = kPS2C_ReadDataPort;
    request->commands[10].inOrOut = 0;
    request->commands[11].command = kPS2C_ReadDataPort;
    request->commands[11].inOrOut = 0;
    request->commands[12].command = kPS2C_ReadDataPort;
    request->commands[12].inOrOut = 0;
	
    request->commandsCount = 13;
    _device->submitRequestAndBlock(request);
	
    if (request->commandsCount == 13) // success?
    {
        returnValue = ((UInt32)request->commands[10].inOrOut << 16) |
		((UInt32)request->commands[11].inOrOut <<  8) |
		((UInt32)request->commands[12].inOrOut);
    }
	
    _device->freeRequest(request);
	
    return returnValue;
}

UInt8 ApplePS2ElantechTouchpad::readRegister( UInt8 readReg )
{

	if( readReg < 0x10 || readReg > 0x26)		return 0;
	else if(readReg > 0x11 && readReg < 0x20)	return 0;
	
	switch(_hardwareVersion)
	{
		case 1:
			//kET_readRegister
			
			break;
		case 2:
			break;
	}
	
	return 0;
}

//